package com.hup.utils.swing.frame;

import com.hup.utils.commons.hardware.MouseUtil;
import com.hup.utils.commons.hardware.ScreenUtil;
import com.hup.utils.commons.start.LookThemeUtil;
import com.hup.utils.swing.ComponentUtil;
import com.hup.utils.swing.FrameUtil;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

import javax.swing.JWindow;
import javax.swing.Timer;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;

/**
 * 实现窗口在屏幕边缘时自动隐藏;类似QQ的效果
 * 原理:
 * .[目标窗口]在边缘时,监听鼠标离开事件,离开后 {@link #hideDelayMouseExited} 毫秒后隐藏窗口
 * .[目标窗口]隐藏时,本类窗口展示在屏幕边缘
 *
 * @author hugan
 * @date 2020/11/6
 * @apiNote .只支持无边框窗口, 因为有边框窗口无法正常判断鼠标进出窗口事件(可能是受到window主题阴影效果影响);
 * .如果窗口突然被覆盖(如锁屏等),无法收到鼠标退出事件时,窗口不会自动隐藏,目前不修复
 */
@Log4j2
public class WindowEdgeHideHelper extends JWindow implements AWTEventListener, ActionListener {
    private static final int SCREEN_EDGE = (int) (8 * LookThemeUtil.getScreenScale());
    /**
     * 目标窗口
     */
    private final Window window;
    /**
     * 定时器,触发动作是[隐藏目标窗口]
     */
    private final Timer hideTargetTimer;
    /**
     * 用于过滤重复的鼠标事件
     */
    private boolean isMouseEntered;
    private EdgeLocation edgeLocation;

    /**
     * 外部切换[目标窗口]显示状态后,n毫秒后隐藏窗口
     */
    @Setter
    private int hideDelayInitial = 1000;
    /**
     * 鼠标离开[目标窗口]后,n毫秒后隐藏窗口
     */
    @Setter
    private int hideDelayMouseExited = 200;
    /**
     * 本窗口的[屏幕边缘突出距离]
     */
    @Setter
    private int grabSize = (int) (4 * LookThemeUtil.getScreenScale());

    /**
     * 逻辑详见 actionPerformed 注释
     */
    private boolean restartByEntryEvent;

    private WindowEdgeHideHelper(Window window) {
        this.window = window;
        hideTargetTimer = new Timer(hideDelayMouseExited, this);
        hideTargetTimer.setRepeats(false);

        getToolkit().addAWTEventListener(this, AWTEvent.MOUSE_EVENT_MASK);
        setAlwaysOnTop(true);
        setSize(grabSize, grabSize);
        setBackground(new Color(5, 206, 255, 80));
        //setVisible(true);//test
    }

    /**
     * 监听鼠标进入/离开窗口
     */
    @Override
    public void eventDispatched(AWTEvent e) {
        if (!(e.getSource() instanceof Component)) return;//过滤托盘图标

        //过滤事件类型
        int id = e.getID();
        boolean matchType = false;
        if (id == MouseEvent.MOUSE_ENTERED || id == MouseEvent.MOUSE_EXITED || id == MouseEvent.MOUSE_RELEASED) matchType = true;
        if (!matchType) return;

        MouseEvent me = (MouseEvent) e;
        if (id == MouseEvent.MOUSE_RELEASED) {
            if (me.getButton() != MouseEvent.BUTTON1) return;
            if (window != e.getSource() && window != ComponentUtil.getParentWindow((Component) e.getSource())) return;

            //log.info("拖拽后.仍在window内={}, {}, [{},{}]", window.getBounds().contains(me.getXOnScreen(), me.getYOnScreen()), window.getBounds(), me.getXOnScreen(), me.getYOnScreen());
            if (!window.getBounds().contains(me.getXOnScreen(), me.getYOnScreen())) {
                isMouseEntered = false;
                hideTargetTimer.start();
            }
            return;
        }

        //log.info("button={}, {}", me.getModifiers(), me.getButton());//button总是为0,不能使用
        if ((me.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
            //log.info("me={}", me);//me.getModifiers()还可能是ctrl等按键,所以需要指定BUTTON1_MASK
            return;//鼠标按下时,不处理(避免拖拽窗口大小时,窗口隐藏了)
        }

        //本窗口的鼠标进入事件
        if (this == e.getSource()) {
            if (id == MouseEvent.MOUSE_ENTERED) mouseEntered(this);
            return;
        }

        //[目标窗口]的鼠标进入/离开事件
        //过滤其他窗口
        if (window != e.getSource() //source也可能是窗口本身
                && window != ComponentUtil.getParentWindow((Component) e.getSource())) {
            //log.info("otherWin={}", e.paramString());
            return;
        }

        //这里的事件是来自window中不同组件的,所以需要isMouseEntered过滤重复事件
        if (id == MouseEvent.MOUSE_ENTERED) {
            if (!isMouseEntered) {
                isMouseEntered = true;
                mouseEntered(window);
            }
        } else {//if (id == MouseEvent.MOUSE_EXITED) {
            if (isMouseEntered) {
                //判断鼠标是否离开[目标窗口]
                boolean contains = window.getBounds().contains(me.getLocationOnScreen());
                if (!contains) {
                    isMouseEntered = false;
                    mouseExited(window);
                }
            }
        }
    }

    /**
     * 鼠标进入[目标窗口,本窗口]
     */
    private void mouseEntered(Window source) {
        log.debug("source={}", source.getClass().getName());
        if (source == this) {//本窗口
            this.setVisible(false);
            fixWindowLocation();
            window.setVisible(true);
            /*
            作用:避免多屏幕时,鼠标快速划过屏幕边缘,[目标窗口]不自动隐藏
            正常划过: 两次事件mouseEntered[本窗口, 目标窗口], mouseExited[目标窗口]
            鼠标快速划过屏幕边缘: mouseEntered[本窗口],无后续事件,会导致[目标窗口]显示后不自动隐藏
             */
            restartByEntryEvent = true;
            hideTargetTimer.restart();
        } else {//目标窗口
            log.debug("timer.stop");
            hideTargetTimer.stop();
        }
    }

    private void fixWindowLocation() {
        Dimension screenSize = ScreenUtil.getScreenSize();
        Point location = window.getLocation();
        int x = location.x;
        int y = location.y;
        //分辨率变小时,window越界
        if (x + window.getWidth() > screenSize.width) {
            x = screenSize.width - window.getWidth();
        }
        if (y + window.getHeight() > screenSize.height) {
            y = screenSize.height - window.getHeight();
        }
        //分辨率变大时,window不靠边
        if (edgeLocation == EdgeLocation.TOP) {
            y = 0;
        } else if (edgeLocation == EdgeLocation.LEFT) {
            x = 0;
        } else if (edgeLocation == EdgeLocation.RIGHT) {
            x = screenSize.width - window.getWidth();
        }
        if (x != location.x || y != location.y) {
            //log.info("fixed, screenSize={}, oldLoc={}, fixLoc=[{},{}]", screenSize, location, x, y);
            window.setLocation(x, y);
        }
    }

    /**
     * 鼠标离开[目标窗口]
     */
    private void mouseExited(Window source) {
        log.debug("source={}", source.getClass().getName());

        Insets screenCoor = ScreenUtil.getScreenUsableCoor();
        edgeLocation = getEdgeLocation(screenCoor);
        log.debug("edgeLocation={}", edgeLocation);
        if (edgeLocation == null) {//不在边缘
            hideTargetTimer.stop();
        } else {
            restartByEntryEvent = false;
            hideTargetTimer.restart();
        }
    }

    /**
     * {@link #mouseExited(Window)} 的定时任务:隐藏目标窗口,显示本窗口
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        Insets screenCoor = ScreenUtil.getScreenUsableCoor();
        edgeLocation = getEdgeLocation(screenCoor);
        log.debug("edgeLocation={}", edgeLocation);
        if (edgeLocation == null) return;

        if (restartByEntryEvent) {
            /*
            mouseEntered(this) --> hideTargetTimer.restart(); 到达此处
            正常情况会触发: mouseEntered(window) -->hideTargetTimer.stop();
                但特定场景时,不会触发mouseEntered(window),导致窗口一直闪烁; (如 joystick打开全屏半透明JWindow)
             */
            boolean insideWindow = window.getBounds().contains(MouseUtil.getLocation());
            log.debug("inside={}", insideWindow);
            if (insideWindow) return;
        }

        //设置本窗口的位置和尺寸
        int x = 0;
        int y = 0;
        int width = grabSize;
        int height = grabSize;
        switch (edgeLocation) {
            case TOP:
                x = window.getX();
                width = window.getWidth();
                break;
            case RIGHT:
                x = screenCoor.right - width + 1;
                //不break
            case LEFT:
                y = window.getY();
                height = window.getHeight();
        }
        log.debug("[{},{}],[{},{}]", x, y, width, height);
        this.setLocation(x, y);
        this.setSize(width, height);

        window.setVisible(false);
        this.setVisible(true);
    }

    /**
     * @return 目标窗口的位置
     */
    private EdgeLocation getEdgeLocation(Insets screenCoor) {
        if (window.getY() <= SCREEN_EDGE) return EdgeLocation.TOP;
        if (window.getX() <= SCREEN_EDGE) return EdgeLocation.LEFT;
        if (window.getX() + window.getWidth() >= screenCoor.right - SCREEN_EDGE) return EdgeLocation.RIGHT;
        return null;
    }

    /**
     * @param b 是否启用功能:false-如[目标窗口]隐藏到托盘时,不起用本类功能
     */
    public void setEnableFunction(boolean b) {
        isMouseEntered = false;//都要重置状态
        if (b) {
            //刷新一次状态
            hideTargetTimer.setInitialDelay(hideDelayInitial);
            restartByEntryEvent = false;
            hideTargetTimer.restart();
            hideTargetTimer.setInitialDelay(hideDelayMouseExited);
        } else {
            hideTargetTimer.stop();
            this.setVisible(false);
        }
    }

    public static WindowEdgeHideHelper bind(Window window) {
        if (!(window instanceof EdgeHideWindow)) throw new IllegalArgumentException("入参窗口必须继承EdgeHideWindow,"
                + "业务切换显示状态时,用switchVisible()代替setVisible()");
        if (!FrameUtil.isUndecorated(window)) throw new IllegalArgumentException("只支持无边框窗口");
        return new WindowEdgeHideHelper(window);
    }

    /**
     * 屏幕边缘状态,优先级按代码顺序
     */
    private enum EdgeLocation {
        TOP, LEFT, RIGHT;
    }

    /**
     * 目标窗口必须继承的接口类,提供 {@link #switchVisible(boolean)}代替原来的 {@link Window#setVisible(boolean)}
     */
    public interface EdgeHideWindow {
        WindowEdgeHideHelper getEdgeHideHelper();

        /**
         * @param b 展示/隐藏目标窗口,
         * @apiNote 建议点击托盘图标时, 入参总是为true
         */
        default void switchVisible(boolean b) {
            ((Window) this).setVisible(b);
            getEdgeHideHelper().setEnableFunction(b);
        }

        /**
         * 通知鼠标已离开窗口;
         * 用于解决[打开新窗口时,本窗口被遮盖了,不能自动隐藏]
         */
        default void notifyMouseExited() {
            getEdgeHideHelper().mouseExited((Window) this);
        }

    }

}
